﻿namespace EveKillboard.Data
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data;
    using System.Diagnostics;
    using System.Linq;
    using System.Linq.Expressions;
    using Core;
    using Core.Exceptions;
    using Core.Interfaces;

    public class DataAccessProvider : IDataAccessProvider, IDisposed, ITraceSource
    {
        private SqlDataContext _dbContext;
        private bool _disposed;

        public TraceSource Trace { get; set; }

        public DataAccessProvider(TraceSource traceSource = null)
        {
            Trace = traceSource ?? new TraceSource("DataAccessProvider");
            _dbContext = new SqlDataContext();
        }

        public void Initialize()
        {
        }

        public bool Exists<T>(Expression<Func<T, bool>> predicate, out T value) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                value = _dbContext.Set<T>().FirstOrDefault(predicate);
                return value != null;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public void Delete<T>(Expression<Func<T, bool>> predicate) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var value = _dbContext.Set<T>().FirstOrDefault(predicate);
                if (value == null) return;
                
                _dbContext.Entry(value).State = EntityState.Deleted;
                _dbContext.SaveChanges();
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public int? GetId<T>(Expression<Func<T, bool>> predicate) where T : class, IId
        {
            this.ThrowIfDisposed();
            try
            {
                var value = _dbContext.Set<T>().FirstOrDefault(predicate);
                return value == null ? (int?)null : value.Id;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public T Find<T>(Expression<Func<T, bool>> predicate) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                return _dbContext.Set<T>().FirstOrDefault(predicate);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public T Find<T>(int id) where T : class, IId
        {
            this.ThrowIfDisposed();
            try
            {
                return _dbContext.Set<T>().FirstOrDefault(v => v.Id == id);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public T Add<T>(T value) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var val = _dbContext.Entry(value);
                val.State = EntityState.Added;
                _dbContext.SaveChanges();
                return val.Entity;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public T Save<T>(T value) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var val = _dbContext.Entry(value);
                val.State = EntityState.Modified;
                _dbContext.SaveChanges();
                return val.Entity;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public IQueryable<T> Query<T>() where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                return _dbContext.Set<T>();
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public IQueryable<T> FindAll<T>(Expression<Func<T, bool>> predicate, bool track = true) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var q = track ? _dbContext.Set<T>().Where(predicate) : _dbContext.Set<T>().AsNoTracking().Where(predicate);
                Trace.TraceEvent(TraceEventType.Verbose, q.ToString());
                return q;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public IEnumerable<T> FindAll<T>(bool track = true) where T : class
        {
            return FindAll<T>(0, 0, track);
        }

        public IEnumerable<T> FindAll<T>(int maxcount = 0, int skip = 0, bool track = true) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var query = track ? _dbContext.Set<T>().AsQueryable() : _dbContext.Set<T>().AsNoTracking().AsQueryable();
                query = skip > 0 ? query.Skip(skip) : query;
                query = maxcount > 0 ? query.Take(maxcount) : query;

                Trace.TraceEvent(TraceEventType.Verbose, query.ToString());

                return query;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public IEnumerable<T> FindAll<T>(Expression<Func<T, bool>> predicate, int maxcount = 0, int skip = 0, bool track = true) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var query = track ? _dbContext.Set<T>().Where(predicate) : _dbContext.Set<T>().AsNoTracking().Where(predicate);
                query = skip > 0 ? query.Skip(skip) : query;
                query = maxcount > 0 ? query.Take(maxcount) : query;

                Trace.TraceEvent(TraceEventType.Verbose, query.ToString());

                return query;
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }
        
        public IEnumerable<TResult> Select<T, TResult>(Expression<Func<T, bool>> predicate, Expression<Func<T, TResult>> selector, bool track = true) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var query = track ? _dbContext.Set<T>().Where(predicate) : _dbContext.Set<T>().AsNoTracking().Where(predicate);

                Trace.TraceEvent(TraceEventType.Verbose, query.ToString());

                return query.Select(selector);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public int? Sum<T>(Expression<Func<T, bool>> predicate, Expression<Func<T, int?>> selector) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var query = _dbContext.Set<T>().AsNoTracking().Where(predicate);

                Trace.TraceEvent(TraceEventType.Verbose,query.ToString());
                
                return query.Sum(selector);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public decimal? Sum<T>(Expression<Func<T, bool>> predicate, Expression<Func<T, decimal?>> selector) where T : class
        {
            this.ThrowIfDisposed();
            try
            {
                var query = _dbContext.Set<T>().AsNoTracking().Where(predicate);

                Trace.TraceEvent(TraceEventType.Verbose, 0, query.ToString());

                return query.Sum(selector);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public int SqlQuery(string sql, object[] parameters)
        {
            this.ThrowIfDisposed();
            try
            {
                return _dbContext.Database.ExecuteSqlCommand(sql, parameters);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }

        }

        public IEnumerable<T> SqlQuery<T>(string sql, object[] parameters)
        {
            this.ThrowIfDisposed();
            try
            {
                return _dbContext.Database.SqlQuery<T>(sql, parameters);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        public IEnumerable SqlQuery(Type elementType, string sql, object[] parameters)
        {
            this.ThrowIfDisposed();
            try
            {
                return _dbContext.Database.SqlQuery(elementType, sql, parameters);
            }
            catch (Exception exp)
            {
                string msg = GetInnerMessage(exp);
                Trace.TraceEvent(TraceEventType.Error, msg);
                throw new DataAccessException(msg, exp);
            }
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            GC.SuppressFinalize(this);

            if (_disposed) return;
            _disposed = true;
            
            if (_dbContext != null)
                _dbContext.Dispose();

            _dbContext = null;

            Trace.TraceEvent(TraceEventType.Verbose, "DataAccessProvider disposed");
        }

        public bool IsDisposed
        {
            get { return _disposed; }
        }

        private static string GetInnerMessage(Exception exp)
        {
            if (exp.InnerException == null) return exp.Message;

            while (exp.InnerException != null)
                exp = exp.InnerException;

            return exp.Message;
        }

        
    }
}